// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/memory/memory_pressure_monitor_chromeos.h"

#include <fcntl.h>
#include <sys/select.h>

#include "base/metrics/histogram_macros.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/process_metrics.h"
#include "base/single_thread_task_runner.h"
#include "base/sys_info.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"

namespace base {
namespace chromeos {

    namespace {

        // The time between memory pressure checks. While under critical pressure, this
        // is also the timer to repeat cleanup attempts.
        const int kMemoryPressureIntervalMs = 1000;

        // The time which should pass between two moderate memory pressure calls.
        const int kModerateMemoryPressureCooldownMs = 10000;

        // Number of event polls before the next moderate pressure event can be sent.
        const int kModerateMemoryPressureCooldown = kModerateMemoryPressureCooldownMs / kMemoryPressureIntervalMs;

        // Threshold constants to emit pressure events.
        const int kNormalMemoryPressureModerateThresholdPercent = 60;
        const int kNormalMemoryPressureCriticalThresholdPercent = 95;
        const int kAggressiveMemoryPressureModerateThresholdPercent = 35;
        const int kAggressiveMemoryPressureCriticalThresholdPercent = 70;

        // The possible state for memory pressure level. The values should be in line
        // with values in MemoryPressureListener::MemoryPressureLevel and should be
        // updated if more memory pressure levels are introduced.
        enum MemoryPressureLevelUMA {
            MEMORY_PRESSURE_LEVEL_NONE = 0,
            MEMORY_PRESSURE_LEVEL_MODERATE,
            MEMORY_PRESSURE_LEVEL_CRITICAL,
            NUM_MEMORY_PRESSURE_LEVELS
        };

        // This is the file that will exist if low memory notification is available
        // on the device.  Whenever it becomes readable, it signals a low memory
        // condition.
        const char kLowMemFile[] = "/dev/chromeos-low-mem";

        // Converts a |MemoryPressureThreshold| value into a used memory percentage for
        // the moderate pressure event.
        int GetModerateMemoryThresholdInPercent(
            MemoryPressureMonitor::MemoryPressureThresholds thresholds)
        {
            return thresholds == MemoryPressureMonitor::THRESHOLD_AGGRESSIVE_CACHE_DISCARD || thresholds == MemoryPressureMonitor::THRESHOLD_AGGRESSIVE
                ? kAggressiveMemoryPressureModerateThresholdPercent
                : kNormalMemoryPressureModerateThresholdPercent;
        }

        // Converts a |MemoryPressureThreshold| value into a used memory percentage for
        // the critical pressure event.
        int GetCriticalMemoryThresholdInPercent(
            MemoryPressureMonitor::MemoryPressureThresholds thresholds)
        {
            return thresholds == MemoryPressureMonitor::THRESHOLD_AGGRESSIVE_TAB_DISCARD || thresholds == MemoryPressureMonitor::THRESHOLD_AGGRESSIVE
                ? kAggressiveMemoryPressureCriticalThresholdPercent
                : kNormalMemoryPressureCriticalThresholdPercent;
        }

        // Converts free percent of memory into a memory pressure value.
        MemoryPressureListener::MemoryPressureLevel GetMemoryPressureLevelFromFillLevel(
            int actual_fill_level,
            int moderate_threshold,
            int critical_threshold)
        {
            if (actual_fill_level < moderate_threshold)
                return MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE;
            return actual_fill_level < critical_threshold
                ? MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE
                : MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL;
        }

        // This function will be called less then once a second. It will check if
        // the kernel has detected a low memory situation.
        bool IsLowMemoryCondition(int file_descriptor)
        {
            fd_set fds;
            struct timeval tv;

            FD_ZERO(&fds);
            FD_SET(file_descriptor, &fds);

            tv.tv_sec = 0;
            tv.tv_usec = 0;

            return HANDLE_EINTR(select(file_descriptor + 1, &fds, NULL, NULL, &tv)) > 0;
        }

    } // namespace

    MemoryPressureMonitor::MemoryPressureMonitor(
        MemoryPressureThresholds thresholds)
        : current_memory_pressure_level_(
            MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE)
        , moderate_pressure_repeat_count_(0)
        , moderate_pressure_threshold_percent_(
              GetModerateMemoryThresholdInPercent(thresholds))
        , critical_pressure_threshold_percent_(
              GetCriticalMemoryThresholdInPercent(thresholds))
        , low_mem_file_(HANDLE_EINTR(::open(kLowMemFile, O_RDONLY)))
        , weak_ptr_factory_(this)
    {
        StartObserving();
        LOG_IF(ERROR,
            base::SysInfo::IsRunningOnChromeOS() && !low_mem_file_.is_valid())
            << "Cannot open kernel listener";
    }

    MemoryPressureMonitor::~MemoryPressureMonitor()
    {
        StopObserving();
    }

    void MemoryPressureMonitor::ScheduleEarlyCheck()
    {
        ThreadTaskRunnerHandle::Get()->PostTask(
            FROM_HERE, Bind(&MemoryPressureMonitor::CheckMemoryPressure, weak_ptr_factory_.GetWeakPtr()));
    }

    MemoryPressureListener::MemoryPressureLevel
    MemoryPressureMonitor::GetCurrentPressureLevel() const
    {
        return current_memory_pressure_level_;
    }

    // static
    MemoryPressureMonitor* MemoryPressureMonitor::Get()
    {
        return static_cast<MemoryPressureMonitor*>(
            base::MemoryPressureMonitor::Get());
    }

    void MemoryPressureMonitor::StartObserving()
    {
        timer_.Start(FROM_HERE,
            TimeDelta::FromMilliseconds(kMemoryPressureIntervalMs),
            Bind(&MemoryPressureMonitor::
                     CheckMemoryPressureAndRecordStatistics,
                weak_ptr_factory_.GetWeakPtr()));
    }

    void MemoryPressureMonitor::StopObserving()
    {
        // If StartObserving failed, StopObserving will still get called.
        timer_.Stop();
    }

    void MemoryPressureMonitor::CheckMemoryPressureAndRecordStatistics()
    {
        CheckMemoryPressure();

        // Record UMA histogram statistics for the current memory pressure level.
        MemoryPressureLevelUMA memory_pressure_level_uma(MEMORY_PRESSURE_LEVEL_NONE);
        switch (current_memory_pressure_level_) {
        case MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE:
            memory_pressure_level_uma = MEMORY_PRESSURE_LEVEL_NONE;
            break;
        case MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE:
            memory_pressure_level_uma = MEMORY_PRESSURE_LEVEL_MODERATE;
            break;
        case MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL:
            memory_pressure_level_uma = MEMORY_PRESSURE_LEVEL_CRITICAL;
            break;
        }

        UMA_HISTOGRAM_ENUMERATION("ChromeOS.MemoryPressureLevel",
            memory_pressure_level_uma,
            NUM_MEMORY_PRESSURE_LEVELS);
    }

    void MemoryPressureMonitor::CheckMemoryPressure()
    {
        MemoryPressureListener::MemoryPressureLevel old_pressure = current_memory_pressure_level_;

        // If we have the kernel low memory observer, we use it's flag instead of our
        // own computation (for now). Note that in "simulation mode" it can be null.
        // TODO(skuhne): We need to add code which makes sure that the kernel and this
        // computation come to similar results and then remove this override again.
        // TODO(skuhne): Add some testing framework here to see how close the kernel
        // and the internal functions are.
        if (low_mem_file_.is_valid() && IsLowMemoryCondition(low_mem_file_.get())) {
            current_memory_pressure_level_ = MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL;
        } else {
            current_memory_pressure_level_ = GetMemoryPressureLevelFromFillLevel(
                GetUsedMemoryInPercent(),
                moderate_pressure_threshold_percent_,
                critical_pressure_threshold_percent_);

            // When listening to the kernel, we ignore the reported memory pressure
            // level from our own computation and reduce critical to moderate.
            if (low_mem_file_.is_valid() && current_memory_pressure_level_ == MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL) {
                current_memory_pressure_level_ = MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE;
            }
        }

        // In case there is no memory pressure we do not notify.
        if (current_memory_pressure_level_ == MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE) {
            return;
        }
        if (old_pressure == current_memory_pressure_level_) {
            // If the memory pressure is still at the same level, we notify again for a
            // critical level. In case of a moderate level repeat however, we only send
            // a notification after a certain time has passed.
            if (current_memory_pressure_level_ == MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE && ++moderate_pressure_repeat_count_ < kModerateMemoryPressureCooldown) {
                return;
            }
        } else if (current_memory_pressure_level_ == MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE && old_pressure == MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL) {
            // When we reducing the pressure level from critical to moderate, we
            // restart the timeout and do not send another notification.
            moderate_pressure_repeat_count_ = 0;
            return;
        }
        moderate_pressure_repeat_count_ = 0;
        MemoryPressureListener::NotifyMemoryPressure(current_memory_pressure_level_);
    }

    // Gets the used ChromeOS memory in percent.
    int MemoryPressureMonitor::GetUsedMemoryInPercent()
    {
        base::SystemMemoryInfoKB info;
        if (!base::GetSystemMemoryInfo(&info)) {
            VLOG(1) << "Cannot determine the free memory of the system.";
            return 0;
        }
        // TODO(skuhne): Instead of adding the kernel memory pressure calculation
        // logic here, we should have a kernel mechanism similar to the low memory
        // notifier in ChromeOS which offers multiple pressure states.
        // To track this, we have crbug.com/381196.

        // The available memory consists of "real" and virtual (z)ram memory.
        // Since swappable memory uses a non pre-deterministic compression and
        // the compression creates its own "dynamic" in the system, it gets
        // de-emphasized by the |kSwapWeight| factor.
        const int kSwapWeight = 4;

        // The total memory we have is the "real memory" plus the virtual (z)ram.
        int total_memory = info.total + info.swap_total / kSwapWeight;

        // The kernel internally uses 50MB.
        const int kMinFileMemory = 50 * 1024;

        // Most file memory can be easily reclaimed.
        int file_memory = info.active_file + info.inactive_file;
        // unless it is dirty or it's a minimal portion which is required.
        file_memory -= info.dirty + kMinFileMemory;

        // Available memory is the sum of free, swap and easy reclaimable memory.
        int available_memory = info.free + info.swap_free / kSwapWeight + file_memory;

        DCHECK(available_memory < total_memory);
        int percentage = ((total_memory - available_memory) * 100) / total_memory;
        return percentage;
    }

} // namespace chromeos
} // namespace base
